package materials.network;

import java.io.Serializable;
import java.util.Set;

import exceptions.NoValidInputException;

import materials.matrix.MatrixMaterial;
import materials.matrix.MatrixMaterialInterface;
import materials.neurons.Neurons;


public class NeuronalNetwork implements NetworkInterface, Serializable{
 
	private int _neuronnnumber; //Die Anzahl der NEuronen im Netz
	private int _layers; //Die ANzahl der Schichten
	private StringBuffer[] _vectornames; //Zu jeder Schicht ein Folge von Benamungen der Neuronen (Vektoriell)
	private StringBuffer[] _neurontypes;
	private MatrixMaterialInterface[] _weightmatrizen; //Die Theoretisch n-1 gewichtsmatrizen zur n Schichten
	private MatrixMaterialInterface[] _connectionmatrizen; //Die Theoretisch n-1 Verbindungsmatrizen zur n Schichten

	
	public NeuronalNetwork(){
		_neuronnnumber = 0;
		_layers = 0;
		_neurontypes = new StringBuffer[0];
		_vectornames = new StringBuffer[0];
		_weightmatrizen = new MatrixMaterial[0]; // Es werden keine Matrizen angelegt
		_connectionmatrizen = new MatrixMaterial[0];
		
	}
	 

	public NetworkInterface clone(){
		NeuronalNetwork cloned = new NeuronalNetwork();
		cloned._connectionmatrizen = _connectionmatrizen.clone();
		cloned._weightmatrizen = _weightmatrizen.clone();
		cloned._layers = _layers;
		cloned._neuronnnumber = _neuronnnumber;
		cloned._vectornames = _vectornames.clone();
		cloned._neurontypes = _neurontypes.clone();
		return cloned;
	}
	
	public void raiseLayer() {
		_layers++;
		raiseNeuronTypes();
		raiseStringBuffer();
		if(_layers > 1){
			raiseMatrixSizes(); 
			// Wenn eine neue schicht eingefügt wird existieren die matrizen, 
			//nun müssen noch die richtigen amtrizen aktualisiert werden
		}
	}
	

	private void raiseNeuronTypes() {
		StringBuffer[] temp = new StringBuffer[_layers];
		for(int i = 0; i < _neurontypes.length; i++){
			temp[i] = new StringBuffer(_neurontypes[i]);
		}
		temp[_layers-1] = new StringBuffer();
		_neurontypes = temp;
	}

	public void decreaseLayer() {
		_layers--;
		decreaseStringBuffer();
		decreaseNeuronTypes();
		if(_layers > 1){
			//entferne die letze matrizen
		}
	}
	
	private void decreaseNeuronTypes() {
		StringBuffer[] temp = new StringBuffer[_layers];
		for(int i = 0; i < _neurontypes.length-1; i++){
			temp[i] = new StringBuffer(_neurontypes[i]);
		}
		_neurontypes = temp;
	}

	private void raiseStringBuffer() {
		StringBuffer[] temp = new StringBuffer[_layers];
		for(int i = 0; i < _vectornames.length; i++){
			temp[i] = new StringBuffer(_vectornames[i]);
		}
		temp[_layers-1] = new StringBuffer();
		_vectornames = temp;
	}
	
	public void decreaseStringBuffer(){
		StringBuffer[] temp = new StringBuffer[_layers];
		for(int i = 0; i < _vectornames.length-1; i++){
			temp[i] = new StringBuffer(_vectornames[i]);
		}
		_vectornames = temp;
	}
	
	/**
	 * 
	 * @param layer - Layer in der logischen bedeutung (1 ist die erste schicht)
	 * @return
	 */
	public String getVectorNamesByLayer(int layer){
		return _vectornames[layer-1].toString();
	}
	
	/**
	 * Die GewichtsMatrix & Verbindungsmatrix aus, für die gilt: number = i Verbindung von i zu i+1
	 * @param layer - Die Schicht n-1 zur schicht n
	 * Beispiel wenn der Parameter 1 angegeben wird, wird die erste verbindungmatrix von 1 zu 2 gesucht
	 * Es muss also sichergestellt werden, das i+1 schichten existieren
	 * printMatrix(1) würde sich also die Matrix von schicht 1 zu 2 ausgebn, 
	 * soweithin die 2te Schicht existiert
	 */
	public void printMatrix(int number){
		if(_layers >= (number+1)){
			System.out.println("ConnectionMatrix:\n");
			_connectionmatrizen[number-1].printMatrix();
			System.out.println("\nWeightMatrix:\n");
			_weightmatrizen[number-1].printMatrix();
		}else{
			System.out.println("Fehler noch keine matrizen vorhanden.");
		}
	}
	
	//liefert die Matrix i zurück
	//Matrix1 wäre dann also number = 1
	public MatrixMaterialInterface getConnectionMatrix(int number) {
		return _connectionmatrizen[number-1];
	}
	
	//das gleiche wie bei der vorherigehenden emthode
	public MatrixMaterialInterface getWeigthMatrix(int number) {
		return _weightmatrizen[number-1];
	}

	public int getLayerNumber() {
		return _layers;
	}

	public int getNeuronNumber() {
		return _neuronnnumber;
	}
	
	public int getNumberOfMatrizes(){
		return _connectionmatrizen.length;
	}
	
	
	public boolean hasLayer(int layer) {
		if(_layers >= layer && layer > 0){
			return true;
		}else{
			return false;
		}
	}
	
//##############################################################//abgenommen
	

	
	private void raiseMatrixSizes()
	{	
		    MatrixMaterialInterface[] conn_temp = new MatrixMaterial[_connectionmatrizen.length+1];
			for(int  i = 0; i < _connectionmatrizen.length;i++){
				conn_temp[i] = _connectionmatrizen[i];
			}
			conn_temp[_connectionmatrizen.length] = new MatrixMaterial(0, 0);
			_connectionmatrizen = conn_temp;
			
			
			MatrixMaterialInterface[] weig_temp = new MatrixMaterial[_weightmatrizen.length+1];
			for(int  i = 0; i < _weightmatrizen.length;i++){
				weig_temp[i] = _weightmatrizen[i];
			}
			weig_temp[_weightmatrizen.length] = new MatrixMaterial(0, 0);
			_weightmatrizen = weig_temp;		
	}
	
	
	// Sollte das Neuron in die Erste eingefügt werden sit das kein problem
	// soltle das neuron, jedoch in die letzte schicht eingefügt worden sein, also
	// so das gilt, das es ja keine weitere schicht gibt, und daher wird das neuron nur in der matrix
	//schicht-1 dazugenommen
	//sonst müssen beide einträge gesetzt werden
	/**
	 * Zwei Schritte
	 */
	public void addNeuron(int layer, String nname, String type) {
		if(_layers >= layer && layer >= 1)
		{
			_vectornames[layer-1].append(";"+nname);
			_neurontypes[layer-1].append(";"+type);			
			_neuronnnumber++;
			if(_layers == layer)
			{
				fillInMatrixM(layer-1);
			}
			else if(layer == 1)
			{
				fillInMatrixN(layer);
				
			}
			else
			{
				fillInMatrixN(layer);
				fillInMatrixM(layer-1);
			}
		}
		else
		{
			throw new IllegalArgumentException();
		}
	}
	
	public void printNeuronTypes(int layer){
		String[] arr = _neurontypes[layer-1].toString().substring(1,_neurontypes[layer-1].length()).split(";");
		for(String h : arr){
			System.out.println(h);
		}
	}
	
	
	private void fillInMatrixM(int layer){
		{
			int oldn = _connectionmatrizen[layer-1].getN();
			int oldm = _connectionmatrizen[layer-1].getM();
			
			MatrixMaterialInterface _temp = new MatrixMaterial(oldm+1,oldn);
			for(int m = 1; m <= oldm;m++){
				for(int n = 1; n <= oldn;n++){
					_temp.setValue(m,n,_connectionmatrizen[layer-1].getValue(m, n));
				}
			}
			for(int n = 1; n <= oldn;n++){
					_temp.setValue(oldm+1,n,0);
			}
			_connectionmatrizen[layer-1] = _temp;
		}
		{
			int oldn = _weightmatrizen[layer-1].getN();
			int oldm = _weightmatrizen[layer-1].getM();
			
			MatrixMaterialInterface _temp = new MatrixMaterial(oldm+1,oldn);
			for(int m = 1; m <= oldm;m++){
				for(int n = 1; n <= oldn;n++){
					_temp.setValue(m,n,_weightmatrizen[layer-1].getValue(m, n));
				}
			}
			for(int n = 1; n <= oldn;n++){
					_temp.setValue(oldm+1,n,0);
			}
			_weightmatrizen[layer-1] = _temp;
		}
	}
	
	private void fillInMatrixN(int layer){
		{
			int oldn = _connectionmatrizen[layer-1].getN();
			int oldm = _connectionmatrizen[layer-1].getM();
	
			MatrixMaterialInterface _temp = new MatrixMaterial(oldm,oldn+1);
			for(int m = 1; m <= oldm;m++){
				for(int n = 1; n <= oldn;n++){
					_temp.setValue(m,n,_connectionmatrizen[layer-1].getValue(m, n));
				}
			}
			for(int m = 1; m <= oldm;m++){
					_temp.setValue(m,oldn+1,0);
			}
			_connectionmatrizen[layer-1] = _temp;
		}
		{
			int oldn = _weightmatrizen[layer-1].getN();
			int oldm = _weightmatrizen[layer-1].getM();
	
			MatrixMaterialInterface _temp = new MatrixMaterial(oldm,oldn+1);
			for(int m = 1; m <= oldm;m++){
				for(int n = 1; n <= oldn;n++){
					_temp.setValue(m,n,_weightmatrizen[layer-1].getValue(m, n));
				}
			}
			for(int m = 1; m <= oldm;m++){
					_temp.setValue(m,oldn+1,0);
			}
			_weightmatrizen[layer-1] = _temp;
		}
	}
	
	public void getInfoAboutVectorNames(){
		for(int i = 0; i < _vectornames.length;i++){
			System.out.println((i+1) +": " +_vectornames[i].toString());
		}
	}
	
	public void getInfoAboutMatrizes(){
		for(int i = 0; i < _connectionmatrizen.length; i++){
			System.out.println("ConnectionMatrizes: " + _connectionmatrizen[i].getM() + "x" + _connectionmatrizen[i].getN());
		}
		for(int i = 0; i < _connectionmatrizen.length; i++){
			System.out.println("WeightMatrizes: " +_weightmatrizen[i].getM() + "x" + _connectionmatrizen[i].getN());
		}
	}
//#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+Vorbehalt
	
	private int searchStringPosition(String s, StringBuffer buff){
		int p = 1;
		String[] arr = buff.toString().substring(1, buff.toString().length()).split(";");
		for(String h : arr){
			if(h.equals(s)){
				break;
			}else{
				p++;
			}
		}
		if(p > arr.length){
			return -1;
		}
		return p;
	}
	
	
//****************************************
	
	public void setConnectionFromTo(int layer, String a, String  b) {
		int x = searchStringPosition(a,_vectornames[layer-1]);
		int y = searchStringPosition(b,_vectornames[layer]);
		_connectionmatrizen[layer-1].setValue(y, x, 1.0);
	}


	public boolean hasConnectionFromTo(int layer, String a, String b) {
		int x = searchStringPosition(a, _vectornames[layer-1]);
		int y = searchStringPosition(b, _vectornames[layer]);
		if(_connectionmatrizen[layer-1].getValue(y, x) == 1.0){
			return true;
		}else{
			return false;
		}
	}
	
	public void removeConnectionFromTo(int layer, String a, String b) {
		int x = searchStringPosition(a,_vectornames[layer-1]);
		int y = searchStringPosition(b,_vectornames[layer]);
		_connectionmatrizen[layer-1].setValue(y, x, 0.0);
	}
	
	
	
	public void setWeigthFromTo(int layer, String a, String b, double value) {
		int x = searchStringPosition(a,_vectornames[layer-1]);
		int y = searchStringPosition(b,_vectornames[layer]);
		_weightmatrizen[layer-1].setValue(y, x, value);
	}

	public double getWeigthFromTo(int layer, String a, String b) {
		int x = searchStringPosition(a, _vectornames[layer-1]);
		int y = searchStringPosition(b, _vectornames[layer]);
		return _weightmatrizen[layer-1].getValue(y, x);
	}

	public boolean hasNeuron(int layer,String a) {
		int z = searchStringPosition(a,_vectornames[layer]);
		if(z == -1){
			return false;
		}else{
			return true;
		}
	}
	
	public boolean hasNeuron(String a) {
		boolean haas =  false;
		for(int i = 0; i < _layers; i++){
			int z = searchStringPosition(a,_vectornames[i]);
			if(z != -1){
			haas = true;
			}
		}
		return haas;
	}
	
	public int getElementsInLayer(int layer){
		return _vectornames[layer-1].substring(1, _vectornames[layer-1].length()).split(";").length; 			
	}

	public double[] calculateNetworkInputVector(double[] _vector){
		if(getElementsInLayer(1) == _vector.length){
			for(int i = 0; i < _connectionmatrizen.length; i++){
				//übertragsfunktion
				_vector = calculateUebertrag(_vector, _neurontypes[i].toString());
				MatrixMaterialInterface rechen = calculatePunktualMatrix(_connectionmatrizen[i], _weightmatrizen[i]);
				double[] erg = calculateMatrixWithVector(rechen, _vector);
				_vector = erg;
				//rechne das ergebnisor mit dem vect
			}
			_vector = calculateUebertrag(_vector, _neurontypes[_layers-1].toString());
			return _vector;
		}else{
			throw new NoValidInputException(getElementsInLayer(1) , _vector.length);
		}
	}
	
	
	private double[] calculateUebertrag(double[] vector, String string) {
		double[] ret = new double[vector.length];
		String[] arr = string.substring(1,string.length()).split(";");
		for(int i = 0; i < arr.length; i++){
			ret[i] = calcFunktion(vector[i], arr[i]);
		}	
		return ret;
	}

	private double calcFunktion(double d, String string) {
		double ret = 0;
		if(string.equals(Neurons.Sigmoid)){
			ret = 1.0 /(1.0 + Math.pow(Math.E,-d));
		}
		if(string.equals(Neurons.Binary)){
			ret = 1;
		}
		if(string.equals(Neurons.Linear)){
	 		ret = d;
		}
		if(string.equals(Neurons.Tangential)){
	 		ret = Math.tanh(d);
		}
		return ret;
	}

	public double[] calculateMatrixWithVector(MatrixMaterialInterface m, double[] vect){
		double[] v = new double[m.getM()];
		if(m.getN() == vect.length){
			for(int i = 0; i < m.getM(); i++){
				v[i] = multiplyTwoVector(m.getRow(i),vect);
			}
		}
		return v;
	}
	
	private double multiplyTwoVector(double[] v1, double[] v2){
		double d = 0;
		if(v1.length == v2.length){
			for(int  i = 0; i < v1.length; i++){
				d += v1[i]*v2[i];
			}
		}
		return d;
	}
	
	
	private MatrixMaterialInterface calculatePunktualMatrix(MatrixMaterialInterface m1, MatrixMaterialInterface m2){
		MatrixMaterialInterface ret = new MatrixMaterial(m1.getM(), m1.getN());
		for(int i = 1; i <= m1.getM();i++){
			for (int j = 1; j <= m1.getN(); j++) {
				ret.setValue(i, j, m1.getValue(i, j) * m2.getValue(i, j));
			}
		}
		return ret;
	}








}
